CrewWidgetTab.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. 'use client';
  2. import { useState, useEffect, useCallback } from 'react';
  3. import { fetchApi } from '@/lib/utils/client';
  4. import { useStudioContext } from '@/app/studio/context';
  5. import type { CrewWidgetConfigResponse, CrewWidgetConfigItem } from '@/types/response/crew/widgetConfig';
  6. import { CREW_PERIODS, CREW_WIDGET_THEMES } from '../../widget/constants';
  7. import { type FormState, createEmptyForm, formatDateTime } from '../../widget/types';
  8. import CrewWidgetFormPanel from '../../widget/_components/CrewWidgetFormPanel';
  9. import CrewWidgetPreviewPanel from '../../widget/_components/CrewWidgetPreviewPanel';
  10. import { Button } from '@/components/ui/button';
  11. import { Checkbox } from '@/components/ui/checkbox';
  12. import { Separator } from '@/components/ui/separator';
  13. type Props = { crewID: number };
  14. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  15. export default function CrewWidgetTab({ crewID: _crewID }: Props)
  16. {
  17. const { channelID } = useStudioContext();
  18. const [items, setItems] = useState<CrewWidgetConfigItem[]>([]);
  19. const [loading, setLoading] = useState(true);
  20. const [saving, setSaving] = useState(false);
  21. const [editItem, setEditItem] = useState<CrewWidgetConfigItem|null>(null);
  22. const [showForm, setShowForm] = useState(false);
  23. const [selected, setSelected] = useState<Set<number>>(new Set());
  24. const [form, setForm] = useState<FormState>(createEmptyForm());
  25. const fetchList = useCallback(() => {
  26. if (!channelID) {
  27. setLoading(false);
  28. return;
  29. }
  30. setLoading(true);
  31. fetchApi<CrewWidgetConfigResponse>(`/api/studio/crew/widget/config/${channelID}`)
  32. .then(res => setItems(res.data?.list ?? []))
  33. .catch(() => {})
  34. .finally(() => setLoading(false));
  35. }, [channelID]);
  36. useEffect(() => {
  37. fetchList();
  38. }, [fetchList]);
  39. const getPeriodLabel = (p: number) => CREW_PERIODS.find(x => x.value === p)?.label ?? '-';
  40. const getThemeLabel = (t: number) => CREW_WIDGET_THEMES.find(x => x.value === t)?.label ?? '-';
  41. const openAdd = () => {
  42. setEditItem(null);
  43. setForm(createEmptyForm());
  44. setShowForm(true);
  45. };
  46. const openEdit = (item: CrewWidgetConfigItem) => {
  47. setEditItem(item);
  48. setShowForm(true);
  49. };
  50. const handleSaved = () => {
  51. setShowForm(false);
  52. setEditItem(null);
  53. fetchList();
  54. };
  55. const toggleSelect = (id: number) => {
  56. setSelected(prev => {
  57. const next = new Set(prev);
  58. if (next.has(id)) {
  59. next.delete(id);
  60. } else {
  61. next.add(id);
  62. }
  63. return next;
  64. });
  65. };
  66. const handleBatchDelete = async () => {
  67. if (selected.size === 0) {
  68. return;
  69. }
  70. if (!confirm(`${selected.size}개 설정을 삭제하시겠습니까?`)) {
  71. return;
  72. }
  73. for (const id of selected) {
  74. try {
  75. await fetchApi(`/api/studio/crew/widget/config/${id}/${channelID}`, { method: 'DELETE' });
  76. } catch {
  77. }
  78. }
  79. setSelected(new Set());
  80. fetchList();
  81. };
  82. if (loading) {
  83. return <p className="studio-page__empty">준비 중...</p>;
  84. }
  85. if (showForm) {
  86. return (
  87. <>
  88. <div className="crew-members__toolbar">
  89. <h2 className="crew-members__subtitle">{editItem ? '위젯 수정' : '위젯 추가'}</h2>
  90. <Button variant="outline" size="sm" type="button" onClick={() => setShowForm(false)}>< 뒤로가기</Button>
  91. </div>
  92. <div className="pt-3 pb-3">
  93. <Separator orientation="horizontal" />
  94. </div>
  95. <div className="crew-widget-layout">
  96. <CrewWidgetPreviewPanel form={form} />
  97. <Separator orientation="vertical" />
  98. <CrewWidgetFormPanel
  99. editItem={editItem ?? undefined}
  100. form={form}
  101. onFormChange={setForm}
  102. onSaved={handleSaved}
  103. onCancel={() => setShowForm(false)}
  104. channelID={channelID ?? undefined}
  105. saving={saving}
  106. setSaving={setSaving}
  107. />
  108. </div>
  109. </>
  110. );
  111. }
  112. return (
  113. <div className="crew-widget">
  114. <div className="crew-members__toolbar pb-3">
  115. <h2 className="crew-members__subtitle">위젯 설정 ({items.length}개)</h2>
  116. <div className="studio-page__actions">
  117. {selected.size > 0 && (
  118. <Button variant="destructive" size="sm" type="button" onClick={handleBatchDelete}>{selected.size}개 삭제</Button>
  119. )}
  120. <Button size="sm" type="button" onClick={openAdd}>+ 추가</Button>
  121. </div>
  122. </div>
  123. <div className="studio-page__table-wrap">
  124. <table className="studio-page__table">
  125. <thead>
  126. <tr>
  127. <th><Checkbox checked={items.length > 0 && selected.size === items.length} onCheckedChange={() => setSelected(items.length === selected.size ? new Set() : new Set(items.map(i => i.id)))} /></th>
  128. <th>제목</th>
  129. <th>기간</th>
  130. <th>테마</th>
  131. <th>최대 표시</th>
  132. <th>활성</th>
  133. <th>작업</th>
  134. </tr>
  135. </thead>
  136. <tbody>
  137. {items.length === 0 ? (
  138. <tr><td colSpan={7} className="studio-page__empty">등록된 위젯 설정이 없습니다.</td></tr>
  139. ) : items.map(item => (
  140. <tr key={item.id}>
  141. <td><Checkbox checked={selected.has(item.id)} onCheckedChange={() => toggleSelect(item.id)} /></td>
  142. <td>{item.title}</td>
  143. <td>
  144. {getPeriodLabel(item.period)}
  145. {item.period === 5 && item.startAt && item.endAt && (
  146. <div className="text-xs text-muted-foreground">{formatDateTime(item.startAt)} ~ {formatDateTime(item.endAt)}</div>
  147. )}
  148. </td>
  149. <td>{getThemeLabel(item.theme)}</td>
  150. <td>{item.maxDisplayCount}명</td>
  151. <td><span className={`studio-page__badge studio-page__badge--${item.isActive ? 'active' : 'inactive'}`}>{item.isActive ? '활성' : '비활성'}</span></td>
  152. <td><Button variant="outline" size="sm" type="button" onClick={() => openEdit(item)}>수정</Button></td>
  153. </tr>
  154. ))}
  155. </tbody>
  156. </table>
  157. </div>
  158. </div>
  159. );
  160. }